[예외처리 #2] 예외 적용하기
▶ 왜 예외 처리(Exception Handling)가 중요할까?
백엔드 서버의 핵심 역할은 요청에 대해 적절한 응답을 반환하는 것
그 과정에서 발생할 수 있는 예외를 예측 가능하게 만들고, 사용자와 개발자 모두에게 의미 있는 정보를 주는 것이 중요
- 사용자에게는 쉽고 명확한 메시지
- 개발자에게는 디버깅 가능한 정보
- 시스템에는 일관된 응답 구조
▶ 예외 처리의 기본 원칙
- 예외의 책임을 명확히: 서비스 내부의 예외를 무조건 던지지 말 것
- RuntimeException을 상속한 커스텀 예외로 관리
- 컨트롤러 진입점에서 적절한 응답 포맷으로 반환
- 에러 코드 + 메시지로 트래킹 용이하게 구성
▼ 통일된 예외 응답 포맷 설계
{
"code": "ERROR_CODE",
"message": "사용자 메시지",
"data": "개발자 디버깅 메시지 or 유효성 에러 정보"
}
실전 예외 처리 구성
[Client] → [Controller] → [Service] → 예외 발생(throw)
│
▼
[@RestControllerAdvice] (@ExceptionHandler로 catch)
│
▼
통일된 ResponseDto(code, message, data) 생성
│
▼
[Client] 응답 반환▷ **@RestControllerAdvice( 글로벌 예외 처리 )**
- RestControllerAdvice를 이용하여 Controlle에서 발생하는 예외처리를 Catch하여 설계한 예외처리 방식대로 처리
- @ExceptionHandler을 통해 예외 Catch
- ResponseEntity에 ResponseDto라는 통일된 응답용 Dto를 이용하여 CODE에 따라 메시지 응답 전달
- 즉, 결국 Contrller에서 응답코드를 보내므로 Client 도달하기전에 중간에 위치한다면 통일된 규격으로 예외처리가 가능하다
@RestControllerAdvice
@Slf4j
public class CustomExceptionHandler {
@ExceptionHandler(IOException.class)
public ResponseEntity<?> handleIOException(IOException ex) {
log.warn("IOException : {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ResponseDto<>(Constant.ERROR_CODE, "입출력 오류 발생", ex.getMessage()));
}
@ExceptionHandler({NullPointerException.class, NoSuchElementException.class})
public ResponseEntity<?> handleResourceNotFound(RuntimeException ex) {
log.warn("리소스 없음 : {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ResponseDto<>(Constant.ERROR_CODE, "리소스를 찾을 수 없음", ex.getMessage()));
}
@ExceptionHandler(CustomApiException.class)
public ResponseEntity<?> handleCustomApiException(CustomApiException ex) {
log.warn("잘못된 요청 : {}", ex.getMessage());
return ResponseEntity.badRequest()
.body(new ResponseDto<>(Constant.ERROR_CODE, "잘못된 요청", ex.getMessage()));
}
//...
}
}
→ 반영된 예외처리 결과
{
"code": -1,
"msg": "refresh token expired",
"data": ""
}
▷ 추가) AOP를 활용한 검증 예외 처리
왜 AOP를 사용하는가?
- 컨트롤러마다 반복되는
BindingResult체크 제거 - 코드 간결화 & 일관된 예외 처리
@Aspect
@Component
@Slf4j
public class RequestBodyValidatorAspect {
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void postMapping() {}
@Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)")
public void putMapping() {}
@Around("postMapping() || putMapping()")
public Object validateRequestBody(ProceedingJoinPoint joinPoint) throws Throwable {
for (Object arg : joinPoint.getArgs()) {
if (arg instanceof BindingResult bindingResult && bindingResult.hasErrors()) {
Map<String, String> errorMap = new HashMap<>();
for (FieldError error : bindingResult.getFieldErrors()) {
log.warn("검증 실패 : {} - {}", error.getField(), error.getDefaultMessage());
errorMap.put(error.getField(), error.getDefaultMessage());
}
throw new CustomValidException("유효성 검사 실패", errorMap);
}
}
return joinPoint.proceed();
}
}
▷ 커스텀 예외 클래스 예시
public class CustomApiException extends RuntimeException {
public CustomApiException(String message) {
super(message);
}
}
public class CustomValidException extends RuntimeException {
private final Map<String, String> errorMap;
public CustomValidException(String message, Map<String, String> errorMap) {
super(message);
this.errorMap = errorMap;
}
public Map<String, String> getErrorMap() {
return errorMap;
}
}
관련 문서
- (토이프로젝트) [예외처리 #1] 예외(Exception) 이해하기 — 본문에서 적용한 Checked/Unchecked 예외 개념을 먼저 정리하는 시리즈 1편
- (토이프로젝트) [Exception] DB 데이터접근 스프링 예외 변환기 — 같은 프로젝트에서 DB 데이터 접근 계층의 예외를 변환·처리하는 사례
- (프레임워크) 공통 예외 처리 - 핵심 개념 및 특징 정리 — 같은
@ExceptionHandler/@ControllerAdvice공통 예외 처리 방식을 다루는 Onz 프로젝트 노트